<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Splines</title>
        <style>
            body {
                margin: 0;
                /* 隐藏body窗口区域滚动条 */
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <script type="module">
            import * as THREE from 'https://cdn.skypack.dev/three@v0.130.1'
            import { OrbitControls } from 'https://cdn.skypack.dev/three@v0.130.1/examples/jsm/controls/OrbitControls.js'
            import { GLTFLoader } from 'https://cdn.skypack.dev/three@v0.130.1/examples/jsm/loaders/GLTFLoader.js'
            import Stats from 'https://cdn.skypack.dev/three@v0.130.1/examples/jsm/libs/stats.module.js'
            import {GUI} from 'https://cdn.skypack.dev/three@v0.130.1/examples/jsm/libs/dat.gui.module.js'
            import { Curves } from 'https://cdn.skypack.dev/three@v0.130.1/examples/jsm/curves/CurveExtras.js'
            
            let tubeGeometry = null
            let position = new THREE.Vector3(), 
                binormal = new THREE.Vector3(), 
                direction = new THREE.Vector3(),
                normal = new THREE.Vector3(),
                lookAt = new THREE.Vector3()
            let cameraEye = null, splineCamera = null
            const params = {
				spline: 'GrannyKnot',
				scale: 4,
				extrusionSegments: 100,
				radiusSegments: 8,
				closed: true,
				animationView: false,
				lookAhead: false,
				cameraHelper: false,
			}
            const splines = {
                CatmullRomCurve3: new THREE.CatmullRomCurve3( [
                    new THREE.Vector3( 0, 10, - 10 ), new THREE.Vector3( 10, 0, - 10 ),
                    new THREE.Vector3( 20, 0, 0 ), new THREE.Vector3( 30, 0, 10 ),
                    new THREE.Vector3( 30, 0, 20 ), new THREE.Vector3( 20, 0, 30 ),
                    new THREE.Vector3( 10, 0, 30 ), new THREE.Vector3( 0, 0, 30 ),
                    new THREE.Vector3( - 10, 10, 30 ), new THREE.Vector3( - 10, 20, 30 ),
                    new THREE.Vector3( 0, 30, 30 ), new THREE.Vector3( 10, 30, 30 ),
                    new THREE.Vector3( 20, 30, 15 ), new THREE.Vector3( 10, 30, 10 ),
                    new THREE.Vector3( 0, 30, 10 ), new THREE.Vector3( - 10, 20, 10 ),
                    new THREE.Vector3( - 10, 10, 10 ), new THREE.Vector3( 0, 0, 10 ),
                    new THREE.Vector3( 10, - 10, 10 ), new THREE.Vector3( 20, - 15, 10 ),
                    new THREE.Vector3( 30, - 15, 10 ), new THREE.Vector3( 40, - 15, 10 ),
                    new THREE.Vector3( 50, - 15, 10 ), new THREE.Vector3( 60, 0, 10 ),
                    new THREE.Vector3( 70, 0, 0 ), new THREE.Vector3( 80, 0, 0 ),
                    new THREE.Vector3( 90, 0, 0 ), new THREE.Vector3( 100, 0, 0 )
                ] ),
                sampleSpline: new THREE.CatmullRomCurve3( [
                    new THREE.Vector3( -100, 10, 0 ), 
                    new THREE.Vector3( -100, 0, 0 ), new THREE.Vector3( 100, 0, 0 ),
                    new THREE.Vector3( 0, 100, 0 ), new THREE.Vector3( -100, 10, 0 )
                ] ),
                GrannyKnot: new Curves.GrannyKnot()

            }
            let Game = {
                tickLength: 16, 				// 20Hz
                lastTick: performance.now(),	// 最后更新时间
                stopKey: 0,
                width: window.innerWidth,
                height: window.innerHeight,
                renderer: null,
                camera: null,
                scene: null,
                group: new THREE.Object3D(),
                clock: new THREE.Clock(),
                stats: new Stats(),
                init: function(){
                    this.scene = new THREE.Scene()

                    /** 相机设置  */
                    let k = this.width / this.height; //窗口宽高比
                    let s = 200; //三维场景显示范围控制系数，系数越大，显示的范围越大
                    // this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 10000);
                    this.camera = new THREE.PerspectiveCamera(50, k, .01, 10000)
                    this.camera.position.set( 0, 0, 500 )
                    this.camera.lookAt( 0, 0, 0 )
                    
                    this.lighting()

                    /** 创建场景对象Scene */
                    this.add()
                    
                    /** 创建渲染器对象 */
                    this.renderer = new THREE.WebGLRenderer()
                    this.renderer.setPixelRatio( window.devicePixelRatio )
                    this.renderer.setSize(this.width, this.height)
                    this.renderer.setClearColor(0xb9d3ff, 1)
                    // this.renderer.setAnimationLoop(render)
                    document.body.appendChild(this.renderer.domElement)
                    document.body.appendChild(this.stats.dom)

                    // 创建控件对象
                    const controls = new OrbitControls(this.camera, this.renderer.domElement)
                    
                    const gui = new GUI();
                    const folderGeometry = gui.addFolder( 'Geometry' );
                    folderGeometry.add( params, 'spline', Object.keys( splines ) ).onChange( function () {
                        Game.genObj();
                    })
                    folderGeometry.add( params, 'scale', 2, 10).step(1).onChange( function () {
                        let mesh = Game.scene.getObjectByName('main');
                        mesh.scale.set(params.scale, params.scale, params.scale)
                    })
                    folderGeometry.add( params, 'extrusionSegments', 50, 500).step(50).onChange( function () {
                        Game.genObj();
                    })
                    folderGeometry.add( params, 'radiusSegments', 2, 12).step(1).onChange( function () {
                        Game.genObj();
                    })
                    folderGeometry.open()
                    // splineCamera
                    const folderCamera = gui.addFolder( 'Camera' )
                    folderCamera.add(params, 'animationView')
                    folderCamera.open()

                    window.onresize = function () {
                        Game.width = window.innerWidth; //窗口宽度
                        Game.height = window.innerHeight; //窗口高度
                        if (Game.renderer == null) return
                        Game.renderer.setSize(Game.width, Game.height);//设置渲染区域尺寸
                        if (Game.camera == null) return
                        k = Game.width / Game.height; //窗口宽高比
                        s = 200
                        //修改相机对象属性
                        Game.camera.left = -s * k
                        Game.camera.right = s * k
                        Game.camera.top = s
                        Game.camera.bottom = -s
                        Game.camera.aspect = k
                        Game.camera.updateProjectionMatrix()
                    }
                    document.onkeydown = function (e) {
                        e = e || window.event;  //标准化事件对象
                        let t = e.target || e.srcElement;  //获取发生事件的元素，兼容IE和DOM
                        if (e.code == "Space") {  // 按下空格键 开始/暂停
                            
                        }
                        console.log(e.code)
                    }
                }, 
                lighting: function(){
                    if (this.scene == null) return
                    /** 光源设置  */
                    // 点光源
                    let point = new THREE.PointLight(0xffffff)
                    point.position.set(400, 200, 300); //点光源位置

                    point.castShadow = true;

                    point.shadow.mapSize.width = 1024;
                    point.shadow.mapSize.height = 1024;

                    const d = 300;

                    point.shadow.camera.left = - d;
                    point.shadow.camera.right = d;
                    point.shadow.camera.top = d;
                    point.shadow.camera.bottom = - d;

                    point.shadow.camera.far = 1000;

                    //点光源添加到场景中
                    this.scene.add(point)
                    //环境光
                    this.scene.add(new THREE.AmbientLight(0x888888))
                },
                update: function(tFrame) {
                    if (tFrame === undefined) {
                        tFrame = performance.now()
                        Game.lastTick = performance.now()
                    }
                    let nextTick = Game.lastTick + Game.tickLength
                    let tickNum = 0
                    if (tFrame > nextTick) {
                        tickNum = Math.floor((tFrame - Game.lastTick) / Game.tickLength)
                    }

                    let looptime = 20 * 1000, offset = 5
                    let cur = Date.now() % looptime / looptime
                    tubeGeometry.parameters.path.getPointAt(cur, position)

                    const segments = tubeGeometry.tangents.length;
                    const pickt = cur * segments;
                    const pick = Math.floor( pickt );
                    const pickNext = ( pick + 1 ) % segments;

                    binormal.subVectors( tubeGeometry.binormals[ pickNext ], tubeGeometry.binormals[ pick ] );
                    binormal.multiplyScalar( pickt - pick ).add( tubeGeometry.binormals[ pick ] );

                    tubeGeometry.parameters.path.getTangentAt( cur, direction );

                    normal.copy( binormal ).cross( direction );
                    position.add( normal.clone().multiplyScalar( offset ) )
                    position.multiplyScalar(params.scale)

                    splineCamera.position.copy(position)
                    cameraEye.position.copy(position)

                    tubeGeometry.parameters.path.getPointAt( ( cur + 30 / tubeGeometry.parameters.path.getLength() ) % 1, lookAt )
                    // lookAt.copy( position ).add( direction )
                    lookAt.multiplyScalar(params.scale)
                    splineCamera.matrix.lookAt( splineCamera.position, lookAt, normal );
				    splineCamera.quaternion.setFromRotationMatrix( splineCamera.matrix )

                    Game.lastTick = tFrame
                    this.stats.update()
                },
                add: function() {
                    let axisHelper = new THREE.AxesHelper(250)
                    this.scene.add(axisHelper)
                    
                    
                    this.genObj()

                    cameraEye = new THREE.Mesh( new THREE.SphereGeometry( 2 ), new THREE.MeshBasicMaterial( { color: 0xdddddd } ) );
                    this.group.add(cameraEye)

                    splineCamera = new THREE.PerspectiveCamera( 84, window.innerWidth / window.innerHeight, 0.01, 1000 )
                    this.group.add(splineCamera)
                    let cameraHelper = new THREE.CameraHelper( splineCamera );
				    this.scene.add( cameraHelper )

                    this.scene.add(this.group)
                },
                genObj: function() {
                    let mesh = this.scene.getObjectByName('main')
                    if (mesh != undefined) {
                        this.scene.remove(mesh)
                        mesh.geometry.dispose()
                    }
                    let path = splines[params.spline]
                    tubeGeometry = new THREE.TubeGeometry( path, params['extrusionSegments'], 2, params.radiusSegments, true );
                    
                    mesh = new THREE.Mesh(tubeGeometry, new THREE.MeshLambertMaterial( { color: 0xff00ff, /*wireframe: true*/ } ))
                    mesh.add(new THREE.Mesh(tubeGeometry, new THREE.MeshBasicMaterial( { color: 0x000000, opacity: 0.3, wireframe: true, transparent: true } )))
                    mesh.name = "main"
                    mesh.scale.set( params.scale, params.scale, params.scale )
                    this.scene.add(mesh)
                }
            }

            function render(tFrame) {
                // clock.getDelta()方法获得两帧的时间间隔
                // mixer.update(clock.getDelta())
                Game.update()
                //执行渲染操作   指定场景、相机作为参数
                Game.renderer.render(Game.scene, params.animationView ? splineCamera : Game.camera);
                // Game.renderer.render(Game.scene, splineCamera);
                Game.stopKey = requestAnimationFrame(render)
            }
            Game.init()
            render()
            // const controls = new OrbitControls(camera, renderer.domElement);//创建控件对象
        </script>
    </body>
</html>